# -*- coding: utf-8 -*-
# Copyright (C) 2009 Henri Wahl <h.wahl@ifw-dresden.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA

##
# This file was part of Nagstamon (http://nagstamon.sf.net)
##
import urllib2
import sys
import socket
import gc
import copy
from Utils import *

try:
    import lxml.etree, lxml.objectify
except Exception, err:
    print
    print err
    print
    print "Could not load lxml.etree, lxml.objectify and lxml.html.clean, maybe you need to install python lxml."
    print
    sys.exit()
# fedora 8 and maybe others use lxml 2 which is more careful and offers more modules
# but which also makes necessary to clean Nagios html output
# if not available should be ok because not needed
try:
    import lxml.html.clean
except:
    pass

          
class NagiosServer(object):
    """
        object of Nagios server - when nagstamon will be able to poll various servers this
        will be useful   
    """
    FETCH_ERRORS=None
    def fetchError(self, err):
        if self.FETCH_ERRORS == None:
            self.FETCH_ERRORS = []
        self.FETCH_ERRORS.append(str(err))

    def getFetchErrors(self):
        return self.FETCH_ERRORS

    def resetFetchError(self):
        self.FETCH_ERRORS = None

    def __init__(self, **kwds):
        # add all keywords to object, every mode searchs inside for its favorite arguments/keywords
        for k in kwds: self.__dict__[k] = kwds[k]

        self.name = ""
        self.nagios_url = ""
        self.nagios_cgi_url = ""
        self.username = ""
        self.password = ""
        self.use_proxy = False
        self.hosts = dict()
        self.hosts_in_maintenance = list()
        self.hosts_acknowledged = list()
        self.new_hosts = dict()
        self.new_hosts_in_maintenance = list()
        self.new_hosts_acknowledged = list()
        self.thread = ""
        self.isChecking = False
        self.debug = False

    
    def __del__(self):
        """
        hopefully a __del__() method may make this object better collectable for gc
        """
        del(self)
        
    
    def GetStatus(self):
        self.resetFetchError()
        """
        get nagios status information from nagcgiurl and give it back
        as dictionary
        """

        # set checking flag to be sure only one thread cares about this server
        self.isChecking = True

        # create filters like described in
        # http://www.nagios-wiki.de/nagios/tips/host-_und_serviceproperties_fuer_status.cgi?s=servicestatustypes
        # hoststatus
        hoststatustypes = 12
        if str(self.conf.filter_all_down_hosts) == "True":
            hoststatustypes = hoststatustypes - 4
        if str(self.conf.filter_all_unreachable_hosts) == "True":
            hoststatustypes = hoststatustypes - 8
        # servicestatus
        servicestatustypes = 28
        if str(self.conf.filter_all_unknown_services) == "True":
            servicestatustypes = servicestatustypes - 8
        if str(self.conf.filter_all_warning_services) == "True":
            servicestatustypes = servicestatustypes - 4
        if str(self.conf.filter_all_critical_services) == "True":
            servicestatustypes = servicestatustypes - 16
        # serviceprops & hostprops both have the same values for the same states so I
        # group them together
        hostserviceprops = 0
        if str(self.conf.filter_acknowledged_hosts_services) == "True":
            hostserviceprops = hostserviceprops + 8
        if str(self.conf.filter_hosts_services_disabled_notifications) == "True":
            hostserviceprops = hostserviceprops + 8192
        if str(self.conf.filter_hosts_services_disabled_checks) == "True":
            hostserviceprops = hostserviceprops + 32
        if str(self.conf.filter_hosts_services_maintenance) == "True":
            hostserviceprops = hostserviceprops + 2
        
        # create Nagios items dictionary with to lists for services and hosts
        # every list will contain a dictionary for every failed service/host
        nagitems = {"services":[], "hosts":[]}

        # services (unknown, warning or critical?)
        nagcgiurl_services = self.nagios_cgi_url + "/status.cgi?host=all&servicestatustypes=" + str(servicestatustypes) + "&serviceprops=" + str(hostserviceprops)
        # hosts (up or down or unreachable)
        nagcgiurl_hosts = self.nagios_cgi_url + "/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=" + str(hoststatustypes) + "&hostprops=" + str(hostserviceprops)
        # fetching hosts in downtime and acknowledged hosts at once is not possible because these 
        # properties get added and nagios display ONLY hosts that have BOTH states
        # hosts that are in scheduled downtime, we will later omit services on those hosts
        # hostproperty 1 = HOST_SCHEDULED_DOWNTIME 
        nagcgiurl_hosts_in_maintenance = self.nagios_cgi_url + "/status.cgi?hostgroup=all&style=hostdetail&hostprops=1"
        # hosts that are acknowledged, we will later omit services on those hosts
        # hostproperty 4 = HOST_STATE_ACKNOWLEDGED 
        nagcgiurl_hosts_acknowledged = self.nagios_cgi_url + "/status.cgi?hostgroup=all&style=hostdetail&hostprops=4"
        # hosts - mostly the down ones
        # unfortunately the hosts status page has a different structure so
        # hosts must be analyzed separately
        try:
            htobj = self.FetchURL(nagcgiurl_hosts, giveback="dict")
            # workaround for Nagios < 2.7 which has an <EMBED> in its output
            # do a copy of a part of htobj into table to be able to delete htobj
            try:
                table = copy.copy(htobj.body.div.table)
            except:
                table = copy.copy(htobj.body.embed.div.table)
            
            # do some cleanup    
            del htobj

            for i in range(1, len(table.tr)):
                try:
                    # ignore empty <tr> rows
                    if not table.tr[i].countchildren() == 1:
                        n = {}
                        # host
                        try:
                            n["host"] = in_unicode(table.tr[i].td[0].table.tr.td.table.tr.td.a.text)
                        except:
                            n["host"] = in_unicode(nagitems[len(nagitems)-1]["host"])
                        # status
                        n["status"] = in_unicode(table.tr[i].td[1].text)
                        # last_check
                        n["last_check"] = in_unicode(table.tr[i].td[2].text)
                        # duration
                        n["duration"] = in_unicode(table.tr[i].td[3].text)
                        # status_information
                        n["status_information"] = in_unicode(table.tr[i].td[4].text)
                        # attempts are not shown in case of hosts so it defaults to "N/A"
                        n["attempt"] = "N/A"
                        
                        # add dictionary full of information about this host item to nagitems
                        nagitems["hosts"].append(n)
                        # after collection data in nagitems create objects from its informations
                        # host objects contain service objects
                        if not self.new_hosts.has_key(n["host"]):
                            self.new_hosts[n["host"]] = NagiosHost()
                            self.new_hosts[n["host"]].name = n["host"]
                            self.new_hosts[n["host"]].status = n["status"]
                            self.new_hosts[n["host"]].last_check = n["last_check"]
                            self.new_hosts[n["host"]].duration = n["duration"]
                            self.new_hosts[n["host"]].attempt = n["attempt"]
                            self.new_hosts[n["host"]].status_information= n["status_information"]
                except Exception, err:
		    print err
                    self.fetchError(err)
                    #pass
                
            # do some cleanup
            del table
            
        except Exception, err:
            # set checking flag back to False
            self.fetchError(err)
            self.isChecking = False
            return "ERROR"

        # services
        try:
            htobj = self.FetchURL(nagcgiurl_services, giveback="dict")

            for i in range(1, len(htobj.body.table[2].tr)):
                try:
                    # ignore empty <tr> rows - there are a lot of them - a Nagios bug? 
                    if not htobj.body.table[2].tr[i].countchildren() == 1:
                        n = {}
                        # host
                        # the resulting table of Nagios status.cgi table omits the
                        # hostname of a failing service if there are more than one
                        # so if the hostname is empty the nagios status item should get
                        # its hostname from the previuos item - one reason to keep "nagitems"
                        try:
                            n["host"] = in_unicode(htobj.body.table[2].tr[i].td[0].table.tr.td.table.tr.td.a.text)
                        except:
                            n["host"] = in_unicode(nagitems["services"][len(nagitems["services"])-1]["host"])
                        # service
                        n["service"] = in_unicode(htobj.body.table[2].tr[i].td[1].table.tr.td.table.tr.td.a.text)
                        # status
                        n["status"] = in_unicode(htobj.body.table[2].tr[i].td[2].text)
                        # last_check
                        n["last_check"] = in_unicode(htobj.body.table[2].tr[i].td[3].text)
                        # duration
                        n["duration"] = in_unicode(htobj.body.table[2].tr[i].td[4].text)
                        # attempt
                        n["attempt"] = in_unicode(htobj.body.table[2].tr[i].td[5].text)
                        # status_information
                        #n["status_information"] = str(htobj.body.table[2].tr[i].td[6].text)
                        try: 
                            n["status_information"] = in_unicode(htobj.body.table[2].tr[i].td[6].text)
                        except Exception ,e :
                            print e
                            n["status_information"] = "unknown"


                        # add dictionary full of information about this service item to nagitems - only if service
                        nagitems["services"].append(n)
                        
                        # after collection data in nagitems create objects of its informations
                        # host objects contain service objects
                        if not self.new_hosts.has_key(n["host"]):
                            self.new_hosts[n["host"]] = NagiosHost()
                            self.new_hosts[n["host"]].name = in_unicode(n["host"])
                            self.new_hosts[n["host"]].status = "OK"
                        # if a service does not exist create its object
                        if not self.new_hosts[n["host"]].services.has_key(n["service"]):
                            self.new_hosts[n["host"]].services[n["service"]] = NagiosService()
                            self.new_hosts[n["host"]].services[n["service"]].host = n["host"]
                            self.new_hosts[n["host"]].services[n["service"]].name = n["service"]
                            self.new_hosts[n["host"]].services[n["service"]].status = n["status"]
                            self.new_hosts[n["host"]].services[n["service"]].last_check = n["last_check"]
                            self.new_hosts[n["host"]].services[n["service"]].duration = n["duration"]
                            self.new_hosts[n["host"]].services[n["service"]].attempt = n["attempt"]
                            self.new_hosts[n["host"]].services[n["service"]].status_information = n["status_information"]
                except Exception, err:
		    print err
                    self.fetchError(err)
                    pass

                                
            # do some cleanup
            del htobj
            
        except Exception, err:
            # set checking flag back to False
            self.isChecking = False
            self.fetchError(err)
            return "ERROR"
       
         # hosts which are in scheduled downtime
        try:
            htobj = self.FetchURL(nagcgiurl_hosts_in_maintenance, giveback="dict")
           
            # workaround for Nagios < 2.7 which has an <EMBED> in its output
            try:
                table = copy.copy(htobj.body.div.table)
            except:
                table = copy.copy(htobj.body.embed.div.table)
            
            # do some cleanup    
            del htobj

            for i in range(1, len(table.tr)):
                try:
                    # ignore empty <tr> rows
                    if not table.tr[i].countchildren() == 1:
                        # host
                        try:
                            self.new_hosts_in_maintenance.append(str(table.tr[i].td[0].table.tr.td.table.tr.td.a.text))
                        except:
                            pass
                except:
                    pass

            # do some cleanup
            del table
        
        except Exception, err:
            # set checking flag back to False
            self.isChecking = False
            self.fetchError(err)
            return "ERROR"
        
        # hosts which are acknowledged
        try:
            htobj = self.FetchURL(nagcgiurl_hosts_acknowledged, giveback="dict")
            
            # workaround for Nagios < 2.7 which has an <EMBED> in its output
            try:
                table = copy.copy(htobj.body.div.table)
            except:
                table = copy.copy(htobj.body.embed.div.table)
            
            # do some cleanup    
            del htobj               

            for i in range(1, len(table.tr)):
                try:
                    # ignore empty <tr> rows
                    if not table.tr[i].countchildren() == 1:
                        # host
                        try:
                            self.new_hosts_acknowledged.append(str(table.tr[i].td[0].table.tr.td.table.tr.td.a.text))
                        except:
                            pass
                except:
                    pass

            # do some cleanup
            del table

        except Exception, err:
            # set checking flag back to False
            self.isChecking = False
            self.fetchError(err)
            return "ERROR"

        # do some cleanup
        self.hosts.clear()
        del self.hosts_acknowledged[::]
        del self.hosts_in_maintenance[::]

        # put new informations into respective dictionaries      
        self.hosts, self.hosts_acknowledged, self.hosts_in_maintenance = copy.copy(self.new_hosts), copy.copy(self.new_hosts_acknowledged), copy.copy(self.new_hosts_in_maintenance)
        
        # after all checks are done unset checking flag
        self.isChecking = False
        
        # debug
        if str(self.conf.debug_mode) == "True":
            print self.name, ":", "got status"
            for h in self.hosts.keys():
                print "\t", h, ":", self.hosts[h].services.keys()
        
        # do some cleanup
        del nagitems
        self.new_hosts.clear()
        del self.new_hosts_acknowledged[::]
        del self.new_hosts_in_maintenance[::]
        gc.collect()
        
        # return True if all worked well    
        return True
    
    
    def FetchURL(self, url, giveback="dict", cgi_data=None):
        """
        get content of given url, cgi_data only used if present
        giveback may be "dict" or "html" - if "dict" FetchURL gives back a dict full of miserable hosts/services,
        if "html" it gives back pure HTML
        if "none" it gives back pure nothing
        existence of cgi_data forces urllib to use POST instead of GET requests
        """
        # using httppasswordmgrwithdefaultrealm because using password in plain
        # url like http://username:password@nagios-server causes trouble with
        # passwords containing special characters like "?"
        # see http://www.voidspace.org.uk/python/articles/authentication.shtml#doing-it-properly
        # attention: the example from above webseite is wrong, passman.add_password needs the 
        # WHOLE URL, with protocol!
        
        passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
        passman.add_password(None, url, self.username, self.password)
        auth_handler = urllib2.HTTPBasicAuthHandler(passman)

        # if something goes wrong with accessing the URL it can be caught
        try:
            # if there should be no proxy used use an empty proxy_handler - only necessary in Windows,
            # where IE proxy settings are used automatically if available
            # In UNIX $HTTP_PROXY will be used
            if str(self.use_proxy_yes) == "False":
                proxy_handler = urllib2.ProxyHandler({})
                urlopener = urllib2.build_opener(auth_handler, proxy_handler)
            else:
                urlopener = urllib2.build_opener(auth_handler)
            
            # create url opener
            urllib2.install_opener(urlopener)

            # use opener - if cgi_data is not empty urllib uses a POST request
            urlcontent = urllib2.urlopen(url, cgi_data)
            
            # give back pure HTML in case giveback is "html"
            if giveback == "html":
                return urlcontent.read()
            
            # give back pure nothing if giveback is "nothing" - useful for POST requests
            if giveback == "nothing":
                return None           
            
            # the heart of the whole Nagios-status-monitoring engine:
            # first step: parse the read HTML
            naghtml = lxml.etree.HTML(urlcontent.read())
            # second step: make pretty HTML of it
            nagprettyhtml = lxml.etree.tostring(copy.copy(naghtml), pretty_print=True)
            
            # third step: clean HTML from tags which embarass libxml2 2.7
            # only possible when module lxml.html.clean has been loaded
            if sys.modules.has_key("lxml.html.clean"):
                # clean html from tags which libxml2 2.7 is worried about
                # this is the case with all tags that do not need a closing end tag like link, br, img
                cleaner = lxml.html.clean.Cleaner(remove_tags=["link", "br", "img"], page_structure=True, style=False)
                nagprettyhtml = copy.copy(cleaner.clean_html(nagprettyhtml))
                
                # lousy workaround for libxml2 2.7 which worries about attributes without value
                # we hope that nobody names a server '" nowrap>' - chances are pretty small because this "name"
                # contains unallowed characters and is far from common sense
                nagprettyhtml = nagprettyhtml.replace('" nowrap>', '">')

            # fourth step: make objects of tags for easy access
            htobj = copy.copy(lxml.objectify.fromstring(nagprettyhtml))
            
        except Exception, err:
            print "HTTP ERROR:", err
            self.fetchError(err)
            return "HTTP ERROR:", err

        #do some cleanup
        del naghtml
        del nagprettyhtml

        # give back HTML object from Nagios webseite
        return htobj
    
    
    def GetHost(self, host):
        """
        find out ip or hostname of given host to access hosts/devices which do not appear in DNS but
        have their ip saved in Nagios
        """
        
        # initialize ip string
        ip = ""

        # glue nagios cgi url and hostinfo 
        nagcgiurl_host  = self.nagios_cgi_url + "/extinfo.cgi?type=1&host=" + host
        
        # get host info
        htobj = self.FetchURL(nagcgiurl_host, giveback="dict")
        
        try:
            # take ip from object path
            ip = str(htobj.body.table.tr.td[1].div[5].text)
            # when connection by DNS is not configured do it by IP
            if str(self.conf.connect_by_dns_yes) == "True":
                # try to get DNS name for ip, if not available use ip
                try:
                    host = socket.gethostbyaddr(ip)[0]
                except Exception, err:
                    host = ip
            else:
                host = ip
        except:
            host = "ERROR"
         
        # do some cleanup
        del htobj    

        # give back host or ip
        return host
    
    
    def __del__(self):
        """
        hopefully a __del__() method may make this object better collectable for gc
        """
        del(self)
    
    
class NagiosHost():
    """
        one host which is monitored by a Nagios server, gets populated with services
    """
    def __init__(self):
        self.name = ""
        self.status = ""
        self.last_check = ""
        self.duration = ""
        self.attempt = ""
        self.status_information = ""
        self.services = dict()
        
        
    def __del__(self):
        """
        hopefully a __del__() method may make this object better collectable for gc
        """
        del(self)
        
        
class NagiosService():
    """
        one service which runs on a host
    """
    def __init__(self):
        self.name = ""
        self.host = ""
        self.status = ""
        self.last_check = ""
        self.duration = ""
        self.attempt = ""
        self.status_information = ""
        
    def __str__(self):
        return  "        [" + self.host + " : " + self.name +" : " + self.status +"]\n"
        
    def __del__(self):
        """
        hopefully a __del__() method may make this object better collectable for gc
        """
        del(self)


class Server(object):
    """
    one Server realized as object for config info
    """
    def __init__(self):
        self.name = ""
        self.nagios_url = ""
        self.nagios_cgi_url = ""
        self.username = ""
        self.password = ""
        self.use_proxy_yes = True
        self.use_proxy_no = False

